FeedReplyItem.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. 'use client';
  2. import Link from 'next/link';
  3. import { useState } from 'react';
  4. import { Heart, MoreHorizontal, CornerDownRight } from 'lucide-react';
  5. import { fetchApi } from '@/lib/utils/client';
  6. import useAuth from '@/hooks/useAuth';
  7. import { formatDate } from '@/lib/utils/client';
  8. import { FeedReply } from '@/types/feed/post';
  9. import {
  10. DropdownMenu,
  11. DropdownMenuContent,
  12. DropdownMenuItem,
  13. DropdownMenuTrigger
  14. } from '@/components/ui/dropdown-menu';
  15. type Props = {
  16. reply: FeedReply;
  17. onReply: (target: FeedReply) => void;
  18. onDelete: (id: number) => void;
  19. };
  20. export default function FeedReplyItem({ reply, onReply, onDelete }: Props) {
  21. const { loginCheck } = useAuth();
  22. const [likes, setLikes] = useState(reply.likes);
  23. const [liked, setLiked] = useState(reply.hasLike);
  24. const [busy, setBusy] = useState(false);
  25. const handleLike = async () => {
  26. if (!loginCheck() || busy || reply.isDeleted) {
  27. return;
  28. }
  29. setBusy(true);
  30. try {
  31. const res = await fetchApi<{ hasLike: boolean; likes: number }>(`/api/feed/comment/${reply.id}/like`, {
  32. method: 'POST',
  33. silent: true
  34. });
  35. if (res.success && res.data) {
  36. setLiked(res.data.hasLike);
  37. setLikes(res.data.likes);
  38. }
  39. } catch (err) {
  40. console.error(err);
  41. } finally {
  42. setBusy(false);
  43. }
  44. };
  45. const handleDelete = async () => {
  46. if (!confirm('이 댓글을 삭제할까요?')) {
  47. return;
  48. }
  49. try {
  50. const res = await fetchApi(`/api/feed/comment/${reply.id}`, {
  51. method: 'DELETE',
  52. silent: true
  53. });
  54. if (res.success) {
  55. onDelete(reply.id);
  56. }
  57. } catch (err) {
  58. console.error(err);
  59. }
  60. };
  61. const authorDisplay = reply.authorName || reply.authorSID || '알 수 없음';
  62. const avatarInitial = (authorDisplay.charAt(0) || '?').toUpperCase();
  63. const parentName = reply.parentAuthorName || reply.parentAuthorSID;
  64. return (
  65. <article className={`feed__reply${reply.parentID ? ' feed__reply--nested' : ''}`}>
  66. {reply.authorSID ? (
  67. <Link href={`/user/${reply.authorSID}`} className="feed__reply-avatar" aria-label={`${authorDisplay} 프로필`}>
  68. {reply.authorThumb ? (
  69. <img src={reply.authorThumb} alt={authorDisplay} />
  70. ) : (
  71. <span className="feed__reply-avatar-fallback">{avatarInitial}</span>
  72. )}
  73. </Link>
  74. ) : (
  75. <span className="feed__reply-avatar">
  76. <span className="feed__reply-avatar-fallback">{avatarInitial}</span>
  77. </span>
  78. )}
  79. <div className="feed__reply-body">
  80. <header className="feed__reply-header">
  81. {reply.authorSID ? (
  82. <Link href={`/user/${reply.authorSID}`} className="feed__reply-author">{authorDisplay}</Link>
  83. ) : (
  84. <span className="feed__reply-author">{authorDisplay}</span>
  85. )}
  86. <span className="feed__reply-time">· {formatDate(reply.createdAt)}</span>
  87. {reply.isOwner && !reply.isDeleted && (
  88. <DropdownMenu>
  89. <DropdownMenuTrigger asChild>
  90. <button type="button" className="feed__reply-more" aria-label="답글 메뉴">
  91. <MoreHorizontal size={16} />
  92. </button>
  93. </DropdownMenuTrigger>
  94. <DropdownMenuContent align="end">
  95. <DropdownMenuItem onClick={handleDelete}>삭제</DropdownMenuItem>
  96. </DropdownMenuContent>
  97. </DropdownMenu>
  98. )}
  99. </header>
  100. {parentName && reply.parentID && (
  101. <div className="feed__reply-parent-ref">
  102. <CornerDownRight size={12} />
  103. <span>{parentName}님에게</span>
  104. </div>
  105. )}
  106. {reply.isDeleted ? (
  107. <p className="feed__reply-content feed__reply-content--deleted">삭제된 답글입니다.</p>
  108. ) : (
  109. <p className="feed__reply-content">{reply.content}</p>
  110. )}
  111. {!reply.isDeleted && (
  112. <footer className="feed__reply-actions">
  113. <button type="button" className={`feed__reply-action${liked ? ' feed__reply-action--active' : ''}`} onClick={handleLike} aria-pressed={liked}>
  114. <Heart size={14} fill={liked ? 'currentColor' : 'none'} />
  115. {likes > 0 && <span>{likes}</span>}
  116. </button>
  117. <button type="button" className="feed__reply-action" onClick={() => onReply(reply)}>답글</button>
  118. </footer>
  119. )}
  120. </div>
  121. </article>
  122. );
  123. }